# js 进阶面试题

# 1、实现图片懒加载的思路

判断图片所在位置是否在可视区域内,图片移到可视区域内进行加载,提供三种判断方法:

  1. offsetTop < clientHeight + scrollTop
  2. element.getBoundingClientRect().top < clientHeight
  3. IntersectionObserver

方案一:clientHeight、scrollTop 和 offsetTop

首先给图片一个占位资源:

<img src="default.jpg" data-src="http://www.xxx.com/target.jpg" />
1

接着,通过监听 scroll 事件来判断图片是否到达视口:

let img = document.getElementsByTagName("img");
let num = img.length;
let count = 0;//计数器,从第一张图片开始计

lazyload();//首次加载别忘了显示图片

window.addEventListener('scroll', lazyload);

function lazyload() {
  let viewHeight = document.documentElement.clientHeight;//视口高度
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;//滚动条卷去的高度
  for(let i = count; i <num; i++) {
    // 元素现在已经出现在视口中
    if(img[i].offsetTop < scrollHeight + viewHeight) {
      if(img[i].getAttribute("src") !== "default.jpg") continue;
      img[i].src = img[i].getAttribute("data-src");
      count ++;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

当然,最好对 scroll 事件做节流处理,以免频繁触发:

// throttle函数我们上节已经实现
window.addEventListener('scroll', throttle(lazyload, 200));
1
2

方案二:getBoundingClientRect

现在我们用另外一种方式来判断图片是否出现在了当前视口, 即 DOM 元素的 getBoundingClientRect API。

上述的 lazyload 函数改成下面这样:

function lazyload() {
  for(let i = count; i <num; i++) {
    // 元素现在已经出现在视口中
    if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
      if(img[i].getAttribute("src") !== "default.jpg") continue;
      img[i].src = img[i].getAttribute("data-src");
      count ++;
    }
  }
}
1
2
3
4
5
6
7
8
9
10

方案三: IntersectionObserver

这是浏览器内置的一个API,实现了监听window的scroll事件、判断是否在视口中以及节流三大功能。

let img = document.getElementsByTagName("img");

const observer = new IntersectionObserver(changes => {
  //changes 是被观察的元素集合
  for(let i = 0, len = changes.length; i < len; i++) {
    let change = changes[i];
    // 通过这个属性判断是否在视口中
    if(change.isIntersecting) {
      const imgElement = change.target;
      imgElement.src = imgElement.getAttribute("data-src");
      observer.unobserve(imgElement);
    }
  }
})
Array.from(img).forEach(item => observer.observe(item));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这样就很方便地实现了图片懒加载,当然这个IntersectionObserver也可以用作其他资源的预加载,功能非常强大。

# 2、表单可以跨域吗?

表单提交是可以进行跨域的,不受浏览器的同源策略限制,估计是历史遗留原因,也有可能是表单提交的结果 js 是拿不到的,所以不限制问题也不大。但是存在一个问题,就是 csrf 攻击,具体不展开了,因为可以自动带上 cookie 造成攻击成功,而 cookie 的新属性 SameSite 就能用来限制这种情况。

# 3、为什么typeof可以检测类型,有没有更好的方法

typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型,这种判断能帮助我们搞定一些问题,js 在底层存储变量的时候会在变量的机器码的低位 1-3 位存储其类型信息(000:对象,010:浮点数,100:字符串,110:布尔,1:整数),但是 null 所有机器码均为 0,直接被当做了对象来看待。

那么有没有更好的办法区分类型呢,一般使用Object.prototype.toString.call()。

# 4、什么是事件委托,它有什么好处?

事件委托是利用事件冒泡机制处理指定一个事件处理程序,来管理某一类型的所有事件 利用冒泡的原理,将事件加到父级身上,触发执行效果,这样只在内存中开辟一块空间,既节省资源又减少 DOM 操作,提高性能 动态绑定事件,列表新增元素不用进行重新绑定了。

# 5、使用js如何改变url,并且页面不刷新?

改变URL的目的是让 js 拿到不同的参数,进行不同的页面渲染,其实就是 vue-router 的原理 最简单的就是改变 hash,改变 hash 是并不会刷新页面的,也会改变URL,也能监听 hashchange 事件进行页面的渲染 还有一种就是使用 HTML5 的 history.pushState() 方法,该方法也可以改变 url 然后不刷新页面。

# 6、以下代码输出结果?

console.log('script start');

const dog = new Promise(function(resolve) {
    console.log('dog1');
    resolve();
    console.log("promiseResolve")
}).then(function() {
    console.log('dog2');
    return "dog"
}).then(console.log("dog end")); // 注意这里没有回调

const cat = new Promise(function(resolve) {
    console.log('cat1');
    resolve();
    setTimeout(() => {
        console.log('setTimeout1')
    })
}).then(function() {
    console.log('cat2');
    return "cat"
})

setTimeout(function() {
    console.log('setTimeout2');
}, 0)

console.log("before promise.race")

Promise.race([dog, cat])
    .then((one, two) => {
        console.log("one", one)
        console.log("two", two)
    })
    .catch(err => {
        console.error('err', err);
    })

console.log('script end');

/*
script start
dog1
promiseResolve
dog end
cat1
before promise.race
script end
dog2
cat2
one cat
two undefined
setTimeout1
setTimeout2
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 7、以下代码输出结果?

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
        console.log("promiseResolve")
    }).then(function() {
        setTimeout(function() {
            console.log('setTimeout1');
        })
        console.log('promise2');
    });
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout2');
}, 0)

async1();

process.nextTick(() => {
    console.log("nextTick");
})

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
    setTimeout(() => {
        console.log('setTimeout3')
    })
}).then(function() {
    console.log('promise4');
})
.then(() => console.log('promise5'))
.then(() => console.log('promise6'))

console.log('script end');

/*
script start
async1 start
promise1
promiseResolve
promise3
nextTick // nextTick有自己的队列,优先于其它微任务先执行
promise2
script end
async1 end
promise4
promise5
promise6
setTimeout2
setTimeout3
setTimeout1
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 8、有N个请求,每次发送一个,如果有一个失败了,就不再执行后面的请求并返回直到上一个请求的所有结果,否则返回所有结果

// 我这里用异步宏任务模拟请求
const result = []
const arr = [1,2,3,4,5,6,7]
function call(data){
  return new Promise((res, rej) => {
    setTimeout(() => {
      if(data === "5"){
        rej(data)
      } else {
        res(data)
      }
    }, 1000)
  })
}
function foo(){
  return call(arr.splice(0, 1).join(""))
    .then((response) => {
      console.log("response", response)
      result.push(response)
      if(arr.length){
        return foo()
      } else {
        console.log("result", result)
        return result
      }
    })
    .catch(err => {
      console.log("result", result)
      return result
    })
}
foo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 9、写一段柯里化函数实现下面的功能

在开始之前,我们首先需要搞清楚函数柯里化的概念。

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

function add(a, b, c){
  return a+b+c
}
curry(add)(1)(2)(3)
curry(add, 1)(2)(3)
curry(add, 1, 2)(3)
curry(add, 1, 2, 3)
1
2
3
4
5
6
7

答案:

function curry(fn, ...args) {
    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if (newArgs.length < length) {
            return curry.call(this,fn,...newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}
console.log(curry(add, 1, 2, 3))
1
2
3
4
5
6
7
8
9
10
11
12
13
const curry = (fn, ...args) => {
  args.length < fn.length
    // 参数长度不足时,重新柯里化该函数,等待接收新参数
	? (...arguments) => curry(fn, ...args, ...arguments)
	// 参数长度满足时,执行函数
	: fn(...args)
}
1
2
3
4
5
6
7

函数柯里化的主要作用:

  • 参数复用
  • 提前返回 – 返回接受余下的参数且返回结果的新函数
  • 延迟执行 – 返回新函数,等待执行

# 10、实现一个方法,用于比较两个版本号(version1、version2)

  • 如果version1 > version2,返回1;如果version1 < version2,返回-1,其他情况返回0
  • 版本号规则x.y.zxyz均为大于等于0的整数,至少有x位
  • 示例:
  • compareVersion('0.1', '1.1.1'); // 返回-1
  • compareVersion('13.37', '1.2 '); // 返回1
  • compareVersion('1.1', '1.1.0'); // 返回0 */
function compareVersion(version1, version2){
   const version1Arr = version1.split(".")
   const version2Arr = version2.split(".")
   const length = Math.max(version1Arr.length, version2Arr.length)
   for(let i=0; i < length; i++){
       if(i<(length-1)){
           if(Number(version1Arr[i]) > Number(version2Arr[i])){
               return 1
           } else if(Number(version1Arr[i]) < Number(version2Arr[i])){
               return -1
           } 
       } else {
           const firstThird = version1Arr[length-1] || 0
           const SecondThird = version2Arr[length-1] || 0
           if(firstThird > SecondThird){
               return 1
           } else if(firstThird < SecondThird){
               return -1
           } else {
               return 0
           }
       }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 11、有一个对象X,内容如下,请写程序找出所有d的值

const X = {
  y: {
    c: {
      d: 1
    }
  },
  z: {
    e: {
      d: 2
    }
  },
  q: {
    f: {
      d: 3
    }
  },
  o: {
    m: {
      g: {
        h: {
          d: 4
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 答案
const resultArr = []
function lookup(obj){
    for(let i in obj){
        if(Object.prototype.toString.call(obj[i]) === '[object Object]'){
            lookup(obj[i])
        } else if(i === "d"){
            resultArr.push(obj.d)
        }
    }
}
lookup(X)
console.log("resultArr", resultArr)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 12、实现以下功能,已知有 N 个 url,每次最多可并行请求 M,尽快完成对 N 个 url 的请求并保存所有返回结果到数组中。(每次发M个请求,每完成一个请求就发送一个剩下的请求)

const urls = ["1", '2', '3', '4', '5', '6', '7']
const results = []
const promises = []
function getData(url) {
    return fetch(url)
        .then((response) => {
            results.push(response)
            if(urls.length) {
                return getData(urls.splice(0, 1).join(""))
            }
        })
}
urls.splice(0, M).forEach(item => {
    promises.push(getData(item))
})
Promise.all(promises)
    .then(() => {
        console.log('results', results)
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 13、怎么计算组件在视口内出现了几次?IntersectionObserver 怎么使用的?怎么知道一个 DOM 节点出现在视口内?

要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它,传统的实现方法是,监听到 scroll 事件后,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。然后声明一个全局变量,每出现一次就加一,就可以得出在视口出现了几次。这种方法的缺点是,由于 scroll 事件密集发生,计算量很大,容易造成性能问题。

于是便有了 IntersectionObserver API

IntersectionObserver

var io = new IntersectionObserver(callback, option);
1

上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点。

// 开始观察
io.observe(document.getElementById("example"));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();
1
2
3
4
5
6
7
8

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

io.observe(elementA);
io.observe(elementB);
1
2

Callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback。 callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

var io = new IntersectionObserver((entries) => {
  console.log(entries);
});
1
2
3

callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。如果同时有两个被观察的对象的可见性发生变化,entries 数组就会有两个成员。

IntersectionObserverEntry 对象提供目标元素的信息,一共有六个属性。

{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

每个属性的含义如下:

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

Option 对象

IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性:

  • threshold 属性
  • root 属性、rootMargin 属性

# 14、浏览器为什么要阻止跨域请求?如何解决跨域?每次跨域请求都需要到达服务端吗?

什么是跨域

跨域是针对浏览器的“同源策略”提出的说法。之所以有“同源策略”这种模式是基于网络安全方面的考虑。所谓的同源策略关注三点:

协议 (http:www.baidu.com & https.www.baidu.com http 协议不同,跨域) 域名 (https://www.aliyun.com & https://developer.aliyun.com 域名不同,跨域) 端口 (http://localhost:8080 & http://localhost:8000 端口号不同,跨域)

哪些网络资源涉及到跨域

“同源策略”对于跨域网络资源的设定非常的清晰。这些场景涉及到跨域禁止操作:

  • 无法获取非同源网页的 cookie、localstorage 和 indexedDB。
  • 无法访问非同源网页的 DOM (iframe)。
  • 无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)。

为什么要阻止跨域呢?上文我们说过是基于安全策略:比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码。

如何解决跨域

针对跨越问题我们该如何解决,主流的方案有以下:

  1. 通过 jsonp 跨域
  2. document.domain + iframe 跨域
  3. location.hash + iframe
  4. window.name + iframe 跨域
  5. postMessage 跨域
  6. 跨域资源共享(CORS)
  7. nginx 代理跨域
  8. nodejs 中间件代理跨域
  9. WebSocket 协议跨域

关于跨域需要明确的问题

跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。

每次需求都会发出,服务器端也会做出响应,只是浏览器端在接受响应的时候会基于同源策略进行拦截。

注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。

# 15、JSONP 的原理是什么?

尽管浏览器有同源策略,但是 <script> 标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。jsonp 通过插入 script 标签的方式来实现跨域,参数只能通过 url 传入,仅能支持 get 请求。

实现原理:

  • Step1: 创建 callback 方法
  • Step2: 插入 script 标签
  • Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法Step4: 前端执行服务端返回的方法调用

jsonp 实现:

function jsonp ({url, params, callback}) {
  return new Promise((resolve, reject) => {
	// 创建 script 标签
	let script = document.createElement('script')
	// 将函数挂在 window 上
	window[callback] = function (data) {
	  resolve(data)
	  // 代码执行后,删除 script 标签
	  document.body.removeChild(script)
	}
	// 回调函数加在请求地址上
	params = {...params, callback} // wb=b&callback=show
	let arrs = []
	for (let key in params) {
	  array.push(`${key}=${params[key]}`)
	}
	script.src = `${url}?${arrs.join('&')}`
	document.body.appendChild(script)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用

function show(data) {
  console.log(data)
}
jsonp({
  url: 'http://localhost:3000/show',
  params:{
	// code 参数
  },
  callback: 'show'
}).then(data => {
  console.log(data)
})
1
2
3
4
5
6
7
8
9
10
11
12

# 16、深拷贝实现

深拷贝最简单的实现是: JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj)) 是最简单的实现方式,但是有一些缺陷:

  • 对象的属性值是函数时,无法拷贝。
  • 原型链上的属性无法拷贝
  • 不能正确的处理 Date 类型的数据
  • 不能处理 RegExp
  • 会忽略 symbol
  • 会忽略 undefined

实现一个 deepClone 函数:

  • 如果是基本数据类型,直接返回
  • 如果是 RegExp 或者 Date 类型,返回对应类型
  • 如果是复杂数据类型,递归。
  • 考虑循环引用的问题
// 递归拷贝
function deepClone(obj, hash = new WeakMap()) {
  if (obj instanceof RegExp) return new RegExp(obj)
  if (obj instanceof Date) return new Date(obj)
  if (obj === null || typeof obj !== 'object') {
	// 如果不是复杂数据类型,直接返回	
    return obj
  }
  if(hash.has(obj)) {
	// 如果已经处理过相同的对象,直接获取(解决循环引用)
	return hash.get(obj)
  }
  /**
   * 如果 obj 是数组,那么 obj.constructor 是 [Function: Array]
   * 如果 obj 是对象,那么 obj.constructor 是 [Function: Object]
   */
  let t = new obj.constructor()
  hash.set(obj, t)
  for (let ikey in obj) {
	// 递归
	if (obj.hasOwnProperty(key)) { // 是否是自身的属性
	  t[key] = deepClone(obj[key], hash)
	}  
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 17、深拷贝怎么解决循环引用问题

看个例子:

function deepCopy(obj){
    const res = Array.isArray(obj) ? [] : {};
    for(let key in obj){
        if(typeof obj[key] === 'object'){
            res[key] = deepCopy(obj[key]);
        }else{
            res[key] = obj[key];
        }
    }
    return res
}
var obj = {
    a:1,
    b:2,
    c:[1,2,3],
    d:{aa:1,bb:2},
};
obj.e = obj;
console.log('obj',obj); // 不会报错

const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

从例子可以看到,当存在循环引用的时候,deepCopy会报错,栈溢出。

obj对象存在循环引用时,打印它时是不会栈溢出

深拷贝obj时,才会导致栈溢出

循环应用问题解决

即:目标对象存在循环应用时报错处理

大家都知道,对象的 key 是不能是对象的。

{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
1
2

参考解决方式一:使用weekmap:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型, 我们可以选择 WeakMap  这种数据结构:

  • 检查 WeakMap  中有无克隆过的对象
  • 有,直接返回
  • 没有,将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function isObject(obj) {
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function cloneDeep(source, hash = new WeakMap()) {
  if (!isObject(source)) return source;
  if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表

  var target = Array.isArray(source) ? [] : {};
  hash.set(source, target); // 新增代码,哈希表设值

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep(source[key], hash); // 新增代码,传入哈希表
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

参考解决方式二:

可以用 Set,发现相同的对象直接赋值,也可用 Map:

const o = { a: 1, b: 2 };
o.c = o;

function isPrimitive(val) {
    return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
    const copied = {};
    for (const [key, value] of Object.entries(obj)) {
        if (isPrimitive(value)) {
            copied[key] = value;
        } else {
            if (set.has(value)) {
                copied[key] = { ...value };
            } else {
                set.add(value);
                copied[key] = clone(value);
            }
        }
    }
    return copied;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 18、 编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
1

# 19、描述一下 Promise:

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。由于它的 then 方法和 catch、finally 方法会返回一个新的 Promise 所以可以允许我们链式调用,解决了传统的回调地狱问题。

关于 then 以及 catch 方法:

  1. Promise的状态一经改变就不能再改变。
  2. .then和.catch都会返回一个新的Promise。
  3. catch不管被连接到哪里,都能捕获上层未捕捉过的错误。
  4. 在Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)。
  5. Promise 的 .then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。
  6. .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。
  7. .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
  8. .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。
  9. .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处
  10. 失败的函数,再某些时候你可以认为catch是.then第二个参数的简便写法。
  11. .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。

finally方法:

  1. .finally()方法不管Promise对象最后的状态如何都会执行
  2. .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道Promise最终的状态是resolved还是rejected的
  3. 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。

最后可以说一下all以及race方法:

  1. Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
  2. .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
  3. Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致。
  4. all和race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。

# 20、手动实现以下 promiseAll和allSeleted

Promise.all = function(promises) {
    const values = []
    let count = 0
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
            Promise.resolve(promise).then(res => {
                count++
                values[index] = res
                if (count === promises.length) {
                    resolve(values)
                }
            }, err => {
                reject(err)
            })
        })
    })
}

Promise.allSeleted = function(promises) {
    let count = 0
    let result = []
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
            Promise.resolve(promise).then(res => {
                result[index] = {
                    value: res,
                    reason: null,
                }
            }, err => {
                result[index] = {
                    value: null,
                    reason: err,
                }
            }).finally(() => {
                count++
                if (count === promises.length) {
                    resolve(result)
                }
            })
        })
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 21、Promise.allSettled 了解吗?手写 Promise.allSettled

  1. Promise.allSettled() 方法接受一组 Promise  实例作为参数,返回一个新的 Promise 实例。
  2. 只有等到所有这些参数实例都返回结果,不管是 fulfilled  还是 rejected ,包装实例才会结束。
  3. 返回的新 Promise  实例,一旦结束,状态总是 fulfilled ,不会变成 rejected 。
  4. 新的 Promise  实例给监听函数传递一个数组 results 。该数组的每个成员都是一个对象,对应传入 Promise.allSettled的 Promise 实例。每个对象都有 status 属性,对应着 fulfilled  和 rejected 。 fulfilled  时,对象有 value  属性, rejected  时有 reason  属性,对应两种状态的返回值。
  5. 有时候我们不关心异步操作的结果,只关心这些操作有没有结束时,这个方法会比较有用。

手写看上题

# 如何实现大文件上传?

如何实现大文件上传? (opens new window)

  • 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片
  • 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件
  • 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听
  • 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度

断点续传

  • 使用 spark-md5 根据文件内容算出文件 hash
  • 通过 hash (根据文件内容生成)可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)
  • 通过 XMLHttpRequest 的 abort 方法暂停切片的上传
  • 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传

# 表单可以跨域么?

在普通的 HTML 表单中,直接提交表单数据是不支持跨域的。这是由于浏览器的同源策略所限制的安全机制。同源策略要求网页只能与同一源(协议、域名和端口号相同)的资源进行交互,以防止恶意网站获取用户的敏感信息。

当表单的目标地址(action)指向不同的域名时,浏览器会阻止直接提交表单,而是会产生一个跨域请求的警告或错误。这种情况下,你需要采取其他方法来处理跨域表单提交。

以下是几种常见的跨域表单提交的解决方案:

  1. 代理:可以通过在同一域名下设置代理服务器,将跨域请求转发到目标域名上。例如,你可以在服务器端设置一个代理脚本,将表单数据发送到目标地址,并将响应返回给客户端。
  2. JSONP:如果目标域名支持 JSONP(JSON with Padding),你可以通过创建一个包含表单数据的动态 <script> 标签,并将目标地址作为脚本的 src 属性值,从而实现跨域请求。
  3. CORS:如果目标域名允许跨域请求,并在响应中设置了适当的 CORS(跨域资源共享)头部,那么浏览器可以发送跨域请求,并将表单数据提交到目标域名。

需要注意的是,以上解决方案都需要在目标域名的服务器端进行相应的配置或支持。具体的实施方法取决于你的项目需求和后端技术栈。

# 观察者和订阅-发布的区别,各⾃⽤在哪⾥?

观察者模式(Observer Pattern)是一种对象间的一对多依赖关系,其中一个对象(称为主题或可观察对象)维护一个对象列表(称为观察者),并在状态发生变化时通知所有观察者。观察者模式中的主题和观察者之间是直接耦合的。主题提供注册、注销和通知观察者的方法,而观察者定义了接收通知并执行相应操作的方法。观察者模式常用于事件处理、GUI 界面、消息系统等场景。

订阅-发布模式(Publish-Subscribe Pattern)也是一种对象间的一对多依赖关系,但通过引入一个中间件(通常称为消息代理或事件总线)来解耦发布者和订阅者。发布者(或称为生产者)将消息发布到中间件,而订阅者(或称为消费者)通过订阅感兴趣的消息类型来接收消息。订阅-发布模式中,发布者和订阅者之间并不直接依赖,它们通过中间件进行通信。这种解耦使得发布者和订阅者可以独立地进行扩展和修改。订阅-发布模式常用于事件驱动架构、消息队列、异步编程等场景。

总结一下它们的区别:

  1. 耦合性:观察者模式中的主题和观察者之间是直接耦合的,而订阅-发布模式中的发布者和订阅者通过中间件解耦。
  2. 扩展性:观察者模式中,主题和观察者之间的关系是固定的,难以动态添加或移除观察者。而在订阅-发布模式中,发布者和订阅者可以独立地进行扩展和修改。
  3. 灵活性:订阅-发布模式相对于观察者模式更加灵活,因为它引入了中间件,可以支持多个发布者和多个订阅者之间的通信。

根据具体的需求和场景,可以选择使用观察者模式或订阅-发布模式。如果需要简单的一对多通信关系,并且不需要动态添加或移除观察者,观察者模式是一个合适的选择。如果需要更灵活的消息传递机制,支持多个发布者和多个订阅者之间的通信,并且希望发布者和订阅者之间解耦,那么订阅-发布模式更适合。

# 简单介绍下 AST抽象语法树

抽象语法树(Abstract Syntax Tree,AST)是源代码的结构化表示,它以树状的形式表示代码的语法结构。AST 是编译器和相关工具中常用的数据结构,用于分析、转换和生成代码。

AST 将源代码解析为一系列的节点,每个节点代表代码中的一个语法结构,例如函数、变量声明、表达式等。每个节点都包含与其对应的语法元素相关的信息,例如标识符、操作符、参数等。

AST 的构建过程通常包括以下步骤:

  1. 词法分析(Lexical Analysis):将源代码分解为一个个的词法单元(tokens),例如关键字、标识符、操作符等。
  2. 语法分析(Syntax Analysis):根据词法单元构建抽象语法树。在这个阶段,解析器会根据语法规则将词法单元组织成语法结构,并生成对应的 AST 节点。
  3. 语义分析(Semantic Analysis):对 AST 进行进一步的分析,检查语法的正确性、类型推断等。在这个阶段,可以进行符号表的构建、类型检查、作用域分析等操作。

AST 的应用非常广泛,包括但不限于以下方面:

  1. 编译器:编译器使用 AST 进行代码的分析、优化和生成。通过对 AST 进行遍历和转换,可以进行代码优化、死代码消除、内联函数等操作。
  2. 代码静态分析:静态分析工具使用 AST 分析代码的结构和潜在问题。例如,代码风格检查工具可以通过分析 AST 来检查代码是否符合指定的代码风格规范。
  3. 代码转换和重构:通过对 AST 进行修改和重构,可以实现代码的转换和重构。例如,将 ES5 代码转换为 ES6 代码,或者进行自动化的代码重构操作。
  4. 代码生成:将 AST 转换为目标代码。编译器将 AST 转换为中间代码或目标代码,用于执行或部署应用程序。

# 如何将 AST 转换为中间代码或目标代码?

将抽象语法树(AST)转换为中间代码或目标代码通常涉及以下步骤:

  1. AST 遍历:遍历 AST 是将其转换为中间代码或目标代码的关键步骤。通过遍历 AST,可以访问和操作每个节点,根据需要进行转换和修改。
  2. 转换规则定义:定义转换规则,将 AST 中的节点映射到中间代码或目标代码的表示形式。这些规则可以根据编程语言、目标平台或转换的具体需求而不同。
  3. 转换操作:根据转换规则,对 AST 的节点进行相应的转换操作。这可能涉及节点的替换、修改、添加或删除等操作。
  4. 代码生成:根据转换后的 AST,生成中间代码或目标代码的表示形式。这可能包括生成新的代码字符串、构建新的代码结构或生成字节码等。

具体的转换过程和工具取决于你使用的编程语言和转换目标。下面是一些常见的工具和技术,用于将 AST 转换为中间代码或目标代码:

  • Babel:Babel 是一个广泛使用的 JavaScript 编译器,它可以将 ES6+ 代码转换为向后兼容的 JavaScript 代码。Babel 使用 AST 进行代码转换。你可以使用 Babel 插件来定义转换规则,并通过 Babel 将 AST 转换为目标代码。
  • TypeScript Compiler API:TypeScript 提供了一个编译器 API,可以使用它来分析和转换 TypeScript 代码。TypeScript 编译器使用 AST 进行代码分析和转换。你可以使用 TypeScript 编译器 API 来访问和修改 AST,并将其转换为中间代码或目标代码。
  • LLVM:LLVM 是一个开源的编译器基础设施,它提供了强大的工具和库,用于生成高质量的中间代码。LLVM 使用自己的中间表示(LLVM IR)来表示代码,并提供了丰富的工具链和库来进行代码转换和优化。
  • 自定义转换工具:如果你有特定的需求或目标,你也可以编写自己的转换工具。这通常涉及编写代码来遍历 AST、定义转换规则和生成目标代码。你可以使用编程语言的解析器和语法分析器来构建 AST,然后根据需求进行转换和代码生成。

无论使用哪种工具或技术,将 AST 转换为中间代码或目标代码需要对语言和目标平台有一定的了解,并具备对 AST 进行遍历和操作的能力。这通常需要对编译原理和相关工具有一定的了解和经验。

# js 定时器为什么是不精确的?

JavaScript中的定时器(setTimeoutsetInterval)也可能不是完全精确的,这是因为JavaScript是单线程的,并且在执行JavaScript代码时,可能会发生以下情况导致定时器不精确:

  1. 事件循环机制:
    • JavaScript使用事件循环机制来处理异步任务。当定时器到期时,会将定时器的回调函数放入任务队列中,等待执行。但是,只有在主线程上的执行栈为空时,才会从任务队列中取出任务执行。因此,如果在定时器到期时主线程正在执行其他任务,定时器的回调函数可能会延迟执行。
  2. 高优先级任务:
    • 如果在定时器到期前有其他高优先级的任务需要执行,例如用户交互事件或网络请求,JavaScript引擎会优先处理这些任务。这可能会导致定时器的回调函数被延迟执行。
  3. 定时器的最小延迟:
    • 根据HTML5规范,setTimeoutsetInterval的最小延迟时间是4毫秒。这意味着无论传递给定时器的延迟值是多少,实际上定时器最早也会在4毫秒后执行。这种最小延迟的存在可能导致定时器的不精确性。
  4. 页面不可见性:
    • 当页面处于不可见状态(例如,用户切换到其他标签或最小化浏览器窗口),浏览器可能会降低对定时器的处理优先级,以节省资源。这可能导致定时器的回调函数被延迟执行或甚至暂停执行。

要提高定时器的精确性,可以考虑以下方法:

  • 使用requestAnimationFrame代替setTimeoutsetInterval,因为requestAnimationFrame会在浏览器的下一帧绘制之前执行,通常可以获得更好的时间精度。
  • 使用Web Workers在后台线程中处理定时任务,以避免主线程的阻塞。
  • 注意避免在定时器回调函数中执行耗时的操作,以确保定时器的回调函数能够及时执行。

尽管如此,仍然无法保证定时器的绝对精确性,因为它受到浏览器和操作系统等因素的限制。